// Copyright 2013 The Gorilla WebSocket Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
	"bytes"
	"fmt"
	"log"
	"net/http"
	"time"

	"github.com/gorilla/websocket"
)

const (
	// Time allowed to write a message to the peer.
	// 10秒内必须把信息写给前端
	writeWait = 10 * time.Second

	// Time allowed to read the next pong message from the peer.
	// 前端确认心跳的等待时间
	pongWait = 60 * time.Second

	// Send pings to peer with this period. Must be less than pongWait.
	// client向前端定时发送心跳确保前端还存在
	pingPeriod = (pongWait * 9) / 10

	// Maximum message size allowed from peer.
	// 消息的长度不能超过512
	maxMessageSize = 512
)

var (
	newline = []byte{'\n'}
	space   = []byte{' '}
)

var upgrader = websocket.Upgrader{
	ReadBufferSize:  1024, // 读缓冲大小
	WriteBufferSize: 1024, // 写缓冲大小
}

// Client is a middleman between the websocket connection and the hub.
type Client struct {
	hub *Hub

	// The websocket connection.
	// websocket连接
	conn *websocket.Conn

	// Buffered channel of outbound messages.
	// 消息管道
	send chan []byte
}

// readPump pumps messages from the websocket connection to the hub.
//
// The application runs readPump in a per-connection goroutine. The application
// ensures that there is at most one reader on a connection by executing all
// reads from this goroutine.
func (c *Client) readPump() {
	defer func() { //收尾工作
		c.hub.unregister <- c //从hub那注销client
		fmt.Printf("close connection to %s\n", c.conn.RemoteAddr().String())
		c.conn.Close() //关闭websocket管道
	}()
	c.conn.SetReadLimit(maxMessageSize) // 一次从管管中读取的最大长度
	// 连接不断的情况下，每隔54秒向浏览器发一次ping，浏览器返回pong
	c.conn.SetReadDeadline(time.Now().Add(pongWait)) //60秒后不允许读
	c.conn.SetPongHandler(func(string) error {
		c.conn.SetReadDeadline(time.Now().Add(pongWait)) //每次收到pong都把deadline往后推迟60秒
		return nil
	})
	for {
		// 如果前端主动断开连接，该行会报错，for循环会退出。注销client时，hub那儿会关闭client.send管道
		_, message, err := c.conn.ReadMessage()
		if err != nil {
			// 如果以意料之外的关闭状态关闭，就打印日志
			if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
				log.Printf("error: %v", err)
			}
			break //只要ReadMessage失败，就关闭websocket管道、注销client，退出
		}
		// 换行符用空格替代，bytes.TrimSpace把首尾连续的空格去掉，-1为全部替换
		message = bytes.TrimSpace(bytes.Replace(message, newline, space, -1))
		// 把信息加入广播管道
		c.hub.broadcast <- message
	}
}

// writePump pumps messages from the hub to the websocket connection.
//
// A goroutine running writePump is started for each connection. The
// application ensures that there is at most one writer to a connection by
// executing all writes from this goroutine.
func (c *Client) writePump() {
	ticker := time.NewTicker(pingPeriod)
	defer func() {
		ticker.Stop() //ticker不用就stop，防止协程泄漏
		fmt.Printf("close connection to %s\n", c.conn.RemoteAddr().String())
		c.conn.Close() //给前端写数据失败，就可以关系连接了
	}()
	for {
		select {
		case message, ok := <-c.send:
			if !ok {
				// The hub closed the channel.
				fmt.Println("管道已经关闭")
				c.conn.WriteMessage(websocket.CloseMessage, []byte{})
				return
			}
			// 10秒内必须把信息写给前端（写到websocket连接里去），否则就关闭连接
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))

			w, err := c.conn.NextWriter(websocket.TextMessage)
			if err != nil {
				return
			}
			w.Write(message)

			// Add queued chat messages to the current websocket message.
			// 有消息一次全写出去
			n := len(c.send)
			for i := 0; i < n; i++ {
				w.Write(newline)
				w.Write(<-c.send)
			}

			// 必须调close，否则下次调用client.conn.NextWriter时本条消息才会发送给浏览器
			if err := w.Close(); err != nil {
				return //结束一切
			}
		case <-ticker.C:
			c.conn.SetWriteDeadline(time.Now().Add(writeWait))
			if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
				return
			}
		}
	}
}

// serveWs handles websocket requests from the peer.
func serveWs(hub *Hub, w http.ResponseWriter, r *http.Request) {
	conn, err := upgrader.Upgrade(w, r, nil)
	if err != nil {
		log.Println(err)
		return
	}
	client := &Client{hub: hub, conn: conn, send: make(chan []byte, 256)}
	client.hub.register <- client

	// Allow collection of memory referenced by the caller by doing all work in
	// new goroutines.
	go client.writePump()
	go client.readPump()
}
